import 'package:code_builder/code_builder.dart';
import 'ast.dart';
import 'gen.dart';
import 'state_machine.dart';

class Yacc extends Gen {
  final Map<String, String> tokenAliases = {};
  final Map<String, String> tokenTypes = {};
  final Map<String, String> ruleTypes = {};
  bool hasUnion = false;
  DartCode unionCode;

  Yacc(Grammar grammar) : super(grammar) {
    parseTokens();
    parseUnion();
    if (!hasUnion) parseTypes();
    ensureAllRulesHaveTypes();
  }

  void parseUnion() {
    var unionOption = grammar.optionsSection.options
        .firstWhere((o) => o.name == 'union', orElse: () => null);

    if (unionOption != null) {
      if (unionOption.values.isEmpty ||
          unionOption.values.first.dartCode == null) {
        throw 'The %union option must be followed by a Dart code block.';
      } else if (hasUnion) {
        throw 'The %union option may only be used once in a Yacc file.';
      } else {
        hasUnion = true;
        unionCode = unionOption.values.first.dartCode;

        if (grammar.optionsSection.options.any((o) => o.name == 'type')) {
          throw 'If the %union option is present, no %type options may be present.';
        }

        settings.enableYystype();

        for (var rule in grammar.rulesSection.rules) {
          ruleTypes[rule.reference] = settings.yystype;
        }
      }
    }
  }

  void parseTokens() {
    for (var option in grammar.optionsSection.options) {
      if (option.name == 'token') {
        if (option.values.isEmpty) {
          throw "Missing identifier after '%token'.";
        } else if (option.values[0].dartCode != null) {
          throw "Value for '%token' must be an identifier, not '{" +
              option.values[0].dartCode.text +
              "}'.";
        } else if (option.values[0].string != null) {
          throw "Value for '%token' must be an identifier, not '" +
              option.values[0].string +
              "'.";
        }

        var name = option.values[0].reference;
        tokenTypes[name] = option.typeAttr ?? 'int';

        if (option.values.length > 1) {
          if (option.values[1].string == null) {
            throw "Alias for '%token $name' must be a string.";
          }
          if (tokenAliases.containsKey(option.values[1].string)) {
            throw "Alias '${option.values[1].string}' is already defined.";
          }
          tokenAliases[option.values[1].string] = name;
        }
      }
    }
  }

  void parseTypes() {
    for (var option in grammar.optionsSection.options) {
      if (option.name == 'type') {
        if (option.typeAttr?.isNotEmpty != true) {
          throw 'Option %type must be followed by a <type> reference. Ex: %type <foo> bar';
        } else if (option.values.isEmpty) {
          throw 'Option %type <${option.typeAttr}> is must be followed by a rule name.';
        } else {
          var value = option.values.first;
          if (value.reference?.isNotEmpty != true) {
          } else {
            ruleTypes[value.reference] = option.typeAttr;
          }
        }
      }
    }
  }

  void ensureAllRulesHaveTypes() {
    for (var rule in grammar.rulesSection.rules) {
      if (!ruleTypes.containsKey(rule.reference)) {
        throw "No return type specified for parser rule '${rule.reference}'.";
      }
    }
  }

  Library build() {
    var stateMachineBuilder = StateMachineBuilder(this);
    return Library((lib) {
      // Add all necessary imports.
      lib.body.addAll([
        Directive.import('dart:collection'),
        Directive.import('package:source_span/source_span.dart'),
      ]);

      // Add all user-defined code up top.
      for (var dartCode in grammar.optionsSection.dartCode) {
        lib.body.add(Code(dartCode.text));
      }

      // Add the yyTokType "enum values".
      int i = 0;
      for (var name in tokenTypes.keys) {
        lib.body.add(literal(i++).assignConst(name, refer('int')).statement);
      }

      var yyvalType = settings.yystype;

      // Define the union, if need be.
      if (hasUnion) {
        lib.body.add(Code('class $yyvalType { ${unionCode.text} }'));
      }

      // Create yyTok type.
      lib.body.add(Code('''
        class ${settings.yytokType} {
          final Match match;
          final FileSpan span;
          final int type;
          $yyvalType value;
          
          ${settings.yytokType}(this.match, this.span, this.type, this.value);
        }
        '''));

      // Define yyval and yylval.
      //
      // Also define the global stack, etc.

      lib.body.addAll([
        Code('$yyvalType ${settings.yyval};'),
        Code('$yyvalType ${settings.yylval};'),
        Code('int _index = -1;'),
        Code('final Queue ${settings.yystack} = Queue();'),
        Code('final Queue<int> _posStack = Queue<int>()..addFirst(-1);'),
        Code('''
        void _mark() {
          _posStack.addFirst(_index);
        }
        
        void _backtrack() {
          _index = _posStack.removeFirst();
        }
        
        bool _next(int type) {
          return !_done && ${settings.yytok}.elementAt(_index).type == type;
        }

        $yyvalType _consume() {
          return ${settings.yytok}.elementAt(++_index).value;
        }
        
        bool get _done => _index >= ${settings.yytok}.length - 1;
        '''),
      ]);

      for (var rule in grammar.rulesSection.rules) {
        var transitions = stateMachineBuilder.compile(rule.reference, rule);
        // lib.body.add(buildRule(rule, grammar, lib));
      }

      // Finally, add any free text.
      lib.body.add(Code(grammar.freeTextSection.text));
    });
  }

  Method buildRule(Rule rule, Grammar grammar, LibraryBuilder lib) {
    return Method((method) {
      method
        ..name = settings.parseMethod(rule.reference)
        ..returns = refer(ruleTypes[rule.reference])
        ..body = Block((block) {
          // TODO: Replace \b\$\$\b with $$.foo

          int i = 0;
          block.statements.add(Code('${ruleTypes[rule.reference]} \$\$;'));

          for (var alt in rule.alternatives) {
            var name = '_${method.name}${i++}';
            var stub = compileAlternative(name, alt, rule, grammar);
            lib.body.add(stub);
            var call = refer(name).call([]);
            block.addExpression(refer(r'$$').assignNullAware(call));
          }

          block.statements.add(refer(r'$$').returned.statement);
        });
    });
  }

  Method compileAlternative(
      String name, Alternative alt, Rule rule, Grammar grammar) {
    return Method((method) {
      method
        ..name = name
        ..returns = refer(ruleTypes[rule.reference])
        ..body = Block((block) {
          // Declare $$ as the output type
          block.statements.add(Code(ruleTypes[rule.reference] + r' $$;'));

          // Mark our position, so we can backtrack.
          block.addExpression(refer('_mark').call([]));

          compileReference(block, rule, 0, alt, grammar);
        });
    });
  }

  void compileReference(
      BlockBuilder block, Rule rule, int i, Alternative alt, Grammar grammar) {
    var ref = alt.references[i];
    String refName;
    if (ref.string != null) {
      refName = tokenAliases[ref.string] ??=
          throw "No token has the alias '${ref.string}'.";
    } else {
      refName = ref.reference;
    }

    var isToken = tokenTypes.containsKey(refName);
    var isRule = grammar.rulesSection.rules.any((r) => r.reference == refName);

    Expression wasMatched;

    // If this is a token, do a _peek(type)
    // todo: assign to $1, $2, $3, ..., $n
    var component = '\$${i + 1}';

    if (isToken) {
      wasMatched = refer('_next').call([refer(refName)]);
    } else if (isRule) {
      // If this is a rule, try to parse it.
      block.statements.add(Code('// ignore: unused_local_variable'));
      block.addExpression(
          (refer(settings.parseMethod(refName)).call([])).assignVar(component));
      wasMatched = refer(component).notEqualTo(literalNull);
    } else {
      throw 'Unknown token or rule name: "$refName". (Expected ${grammar.rulesSection.rules.map((r) => r.reference).join(', ')})';
    }

    // If the expression was not matched, either return null, or throw.
    var ifNotMatchedBlock = Block((block) {
      if (i == 0) {
        // No alternative succeeded, so backtrack.
        block.addExpression(refer('_backtrack').call([]));
        block.addExpression(literalNull.returned);
        // Return null if this alternative was not matched.
      } else {
        // TODO: What about errors?
        block.addExpression(CodeExpression(Code("throw 'todo'")));
      }
    });

    // If it *was* matched, continue parsing...
    var ifMatchedBlock = Block((block) {
      if (isToken) {
        block.statements.add(Code('// ignore: unused_local_variable'));
        block.addExpression(refer('_consume').call([]).assignVar(component));
      }
      // If this is the last rule, we've succeeded...
      if (i >= alt.references.length - 1) {
        if (alt.dartCode != null) {
          block.statements.add(Code(alt.dartCode.text));
        }
        block.statements.add(refer(r'$$').returned.statement);
      } else {
        compileReference(block, rule, i + 1, alt, grammar);
      }
    });

    // Create an if/else
    // https://github.com/dart-lang/code_builder/issues/223
    var ifStmt = 'if (${wasMatched.accept(DartEmitter())}) {' +
        ifMatchedBlock.accept(DartEmitter()).toString() +
        '} else {' +
        ifNotMatchedBlock.accept(DartEmitter()).toString() +
        '}';
    block.statements.add(Code(ifStmt));
  }
}
